/*
 * brin_tuple.c
 *		BRIN 索引中元组的方法实现。
 *
 * 预期用法是，这个文件外部的代码仅处理
 * BrinMemTuples，并通过这个文件中的函数进行
 * 磁盘表示和内存表示之间的转换。
 *
 * NOTES
 *
 * BRIN 元组类似于堆元组，具有几个关键区别。
 * 第一个有趣的区别是元组头部要简单得多，只包含
 * 它的总长度和一个小区域用于标志。此外，存储的数据
 * 与关系元组描述符不完全匹配：对于描述符中的每个
 * 属性，索引元组携带一个任意数量的值，具体取决于
 * opclass。
 *
 * 此外，对于索引关系的每一列，有两个空值位：
 * 一个 (hasnulls) 存储页面范围内是否有任何元组
 * 将该列设置为 null；另一个 (allnulls) 存储
 * 列值是否全部为 null。如果 allnulls 为 true，
 * 则元组数据区域根本不包含该列的值；如果 hasnulls
 * 被设置，则会有值。请注意，空位掩码的大小可能
 * 与数据数组的大小不同。
 *
 * Portions Copyright (c) 1996-2022, PostgreSQL Global Development Group
 * Portions Copyright (c) 1994, Regents of the University of California
 *
 * IDENTIFICATION
 *	  src/backend/access/brin/brin_tuple.c
 */
#include "postgres.h"

#include "access/brin_tuple.h"
#include "access/detoast.h"
#include "access/heaptoast.h"
#include "access/htup_details.h"
#include "access/toast_internals.h"
#include "access/tupdesc.h"
#include "access/tupmacs.h"
#include "utils/datum.h"
#include "utils/memutils.h"


/*
 * 这启用了索引条目的去烘焙。在 VACUUM 足够智能到可以从头重建索引之前是必要的。
 */
#define TOAST_INDEX_HACK


static inline void fc_brin_deconstruct_tuple(BrinDesc *fc_brdesc,
										  char *fc_tp, bits8 *fc_nullbits, bool fc_nulls,
										  Datum *fc_values, bool *fc_allnulls, bool *fc_hasnulls);


/*
 * 返回一个用于磁盘存储 BRIN 元组的元组描述符。
 */
static TupleDesc fc_brtuple_disk_tupdesc(BrinDesc *fc_brdesc)
{
	/* 我们在 BrinDesc 中缓存这些 */
	if (fc_brdesc->bd_disktdesc == NULL)
	{
		int			fc_i;
		int			fc_j;
		AttrNumber	fc_attno = 1;
		TupleDesc	fc_tupdesc;
		MemoryContext fc_oldcxt;

		/* 确保它在 bdesc 的上下文中 */
		fc_oldcxt = MemoryContextSwitchTo(fc_brdesc->bd_context);

		fc_tupdesc = CreateTemplateTupleDesc(fc_brdesc->bd_totalstored);

		for (fc_i = 0; fc_i < fc_brdesc->bd_tupdesc->natts; fc_i++)
		{
			for (fc_j = 0; fc_j < fc_brdesc->bd_info[fc_i]->oi_nstored; fc_j++)
				TupleDescInitEntry(fc_tupdesc, fc_attno++, NULL,
								   fc_brdesc->bd_info[fc_i]->oi_typcache[fc_j]->type_id,
								   -1, 0);
		}

		MemoryContextSwitchTo(fc_oldcxt);

		fc_brdesc->bd_disktdesc = fc_tupdesc;
	}

	return fc_brdesc->bd_disktdesc;
}

/*
 * 生成一个新的磁盘元组以插入到 BRIN 索引中。
 *
 * 如果你接触到这个，请查看 brin_form_placeholder_tuple。
 */
BrinTuple * brin_form_tuple(BrinDesc *fc_brdesc, BlockNumber fc_blkno, BrinMemTuple *fc_tuple,
				Size *fc_size)
{
	Datum	   *fc_values;
	bool	   *fc_nulls;
	bool		fc_anynulls = false;
	BrinTuple  *fc_rettuple;
	int			fc_keyno;
	int			fc_idxattno;
	uint16		fc_phony_infomask = 0;
	bits8	   *fc_phony_nullbitmap;
	Size		fc_len,
				fc_hoff,
				fc_data_len;
	int			fc_i;

#ifdef TOAST_INDEX_HACK
	Datum	   *fc_untoasted_values;
	int			fc_nuntoasted = 0;
#endif

	Assert(fc_brdesc->bd_totalstored > 0);

	fc_values = (Datum *) palloc(sizeof(Datum) * fc_brdesc->bd_totalstored);
	fc_nulls = (bool *) palloc0(sizeof(bool) * fc_brdesc->bd_totalstored);
	fc_phony_nullbitmap = (bits8 *)
		palloc(sizeof(bits8) * BITMAPLEN(fc_brdesc->bd_totalstored));

#ifdef TOAST_INDEX_HACK
	fc_untoasted_values = (Datum *) palloc(sizeof(Datum) * fc_brdesc->bd_totalstored);
#endif

	/*
	 * 为 heap_fill_tuple 设置值/空值数组
	 */
	fc_idxattno = 0;
	for (fc_keyno = 0; fc_keyno < fc_brdesc->bd_tupdesc->natts; fc_keyno++)
	{
		int			fc_datumno;

		/*
		 * 当该列中的任何行都没有非空值时，“allnulls”被设置；当这种情况发生时，没有数据可以存储。因此，设置该列所有数据元素的可空位，并完成操作。
		 */
		if (fc_tuple->bt_columns[fc_keyno].bv_allnulls)
		{
			for (fc_datumno = 0;
				 fc_datumno < fc_brdesc->bd_info[fc_keyno]->oi_nstored;
				 fc_datumno++)
				fc_nulls[fc_idxattno++] = true;
			fc_anynulls = true;
			continue;
		}

		/*
		 * 当数据中有一些空值时，“hasnulls”位被设置。我们仍然需要存储一个实际值，但这意味着我们需要一个空位位图。
		 */
		if (fc_tuple->bt_columns[fc_keyno].bv_hasnulls)
			fc_anynulls = true;

		/* 如果需要，在形成磁盘元组之前序列化值。 */
		if (fc_tuple->bt_columns[fc_keyno].bv_serialize)
		{
			fc_tuple->bt_columns[fc_keyno].bv_serialize(fc_brdesc,
												  fc_tuple->bt_columns[fc_keyno].bv_mem_value,
												  fc_tuple->bt_columns[fc_keyno].bv_values);
		}

		/*
		 * 现在获取每个存储数据的值。注意某些值可能被烘焙，我们不能指望原始堆值能永久存在，因此我们必须去烘焙它们。也尝试对它们进行压缩。
		 */
		for (fc_datumno = 0;
			 fc_datumno < fc_brdesc->bd_info[fc_keyno]->oi_nstored;
			 fc_datumno++)
		{
			Datum		fc_value = fc_tuple->bt_columns[fc_keyno].bv_values[fc_datumno];

#ifdef TOAST_INDEX_HACK

			/* 我们必须查看存储的类型，而不是索引描述符。 */
			TypeCacheEntry *fc_atttype = fc_brdesc->bd_info[fc_keyno]->oi_typcache[fc_datumno];

			/* 我们需要在最后释放该值吗？ */
			bool		fc_free_value = false;

			/* 对于非 varlena 类型，我们不需要做任何特别的事情 */
			if (fc_atttype->typlen != -1)
			{
				fc_values[fc_idxattno++] = fc_value;
				continue;
			}

			/*
			 * 如果值不是 varlena 类型，则不执行任何操作。由于上面的 bv_allnulls，我们不需要关心 NULL 值。
			 *
			 * 如果值以 EXTERNAL 形式存储，则必须提取它，以免依赖外部存储。
			 *
			 * XXX 这实际上是正确的吗？即使对于具有非 NULL 数据的范围，摘要也可能为 NULL 吗？例如，退化的布隆过滤器可能会被丢弃，等等。
			 */
			if (VARATT_IS_EXTERNAL(DatumGetPointer(fc_value)))
			{
				fc_value = PointerGetDatum(detoast_external_attr((struct varlena *)
															  DatumGetPointer(fc_value)));
				fc_free_value = true;
			}

			/*
			 * 如果值超过大小目标，并且是可压缩的数据类型，则尝试在线压缩它。
			 */
			if (!VARATT_IS_EXTENDED(DatumGetPointer(fc_value)) &&
				VARSIZE(DatumGetPointer(fc_value)) > TOAST_INDEX_TARGET &&
				(fc_atttype->typstorage == TYPSTORAGE_EXTENDED ||
				 fc_atttype->typstorage == TYPSTORAGE_MAIN))
			{
				Datum		fc_cvalue;
				char		fc_compression;
				Form_pg_attribute fc_att = TupleDescAttr(fc_brdesc->bd_tupdesc,
													  fc_keyno);

				/*
				 * 如果 BRIN 摘要和被索引属性使用相同数据类型，并且具有有效的压缩方法，我们可以使用相同的压缩方法。否则，我们必须使用默认方法。
				 */
				if (fc_att->atttypid == fc_atttype->type_id)
					fc_compression = fc_att->attcompression;
				else
					fc_compression = InvalidCompressionMethod;

				fc_cvalue = toast_compress_datum(fc_value, fc_compression);

				if (DatumGetPointer(fc_cvalue) != NULL)
				{
					/* 成功的压缩 */
					if (fc_free_value)
						pfree(DatumGetPointer(fc_value));

					fc_value = fc_cvalue;
					fc_free_value = true;
				}
			}

			/*
			 * 如果我们去烘焙/压缩了该值，那么在形成索引元组后我们需要释放它。
			 */
			if (fc_free_value)
				fc_untoasted_values[fc_nuntoasted++] = fc_value;

#endif

			fc_values[fc_idxattno++] = fc_value;
		}
	}

	/* 断言我们没有过度使用临时数组 */
	Assert(fc_idxattno <= fc_brdesc->bd_totalstored);

	/* 计算所需的总空间 */
	fc_len = SizeOfBrinTuple;
	if (fc_anynulls)
	{
		/*
		 * 我们需要一个双长度的位图用于磁盘 BRIN 索引元组；前一半存储“allnulls”位，第二部分存储“hasnulls”。
		 */
		fc_len += BITMAPLEN(fc_brdesc->bd_tupdesc->natts * 2);
	}

	fc_len = fc_hoff = MAXALIGN(fc_len);

	fc_data_len = heap_compute_data_size(fc_brtuple_disk_tupdesc(fc_brdesc),
									  fc_values, fc_nulls);
	fc_len += fc_data_len;

	fc_len = MAXALIGN(fc_len);

	fc_rettuple = palloc0(fc_len);
	fc_rettuple->bt_blkno = fc_blkno;
	fc_rettuple->bt_info = fc_hoff;

	/* 断言 hoff 适合可用空间 */
	Assert((fc_rettuple->bt_info & BRIN_OFFSET_MASK) == fc_hoff);

	/*
	 * heap_fill_tuple 计算的 infomask 和 null 位图对我们来说是无用的。然而，该函数不会接受空的 infomask；我们需要传递一个有效的 null 位图，以便它正确跳过数据区域中的空属性输出。
	 */
	heap_fill_tuple(fc_brtuple_disk_tupdesc(fc_brdesc),
					fc_values,
					fc_nulls,
					(char *) fc_rettuple + fc_hoff,
					fc_data_len,
					&fc_phony_infomask,
					fc_phony_nullbitmap);

	/* 完成这些 */
	pfree(fc_values);
	pfree(fc_nulls);
	pfree(fc_phony_nullbitmap);

#ifdef TOAST_INDEX_HACK
	for (fc_i = 0; fc_i < fc_nuntoasted; fc_i++)
		pfree(DatumGetPointer(fc_untoasted_values[fc_i]));
#endif

	/*
	 * 现在填充真实的空值位掩码。首先是allnulls。
	 */
	if (fc_anynulls)
	{
		bits8	   *fc_bitP;
		int			fc_bitmask;

		fc_rettuple->bt_info |= BRIN_NULLS_MASK;

		/*
		 * 请注意，我们在这个模块中反转空值位的意义：我们
		 * 为空属性存储1而不是0。因此，我们也必须反转
		 * brin_deconstruct_tuple中的att_isnull测试的意义。
		 */
		fc_bitP = ((bits8 *) ((char *) fc_rettuple + SizeOfBrinTuple)) - 1;
		fc_bitmask = HIGHBIT;
		for (fc_keyno = 0; fc_keyno < fc_brdesc->bd_tupdesc->natts; fc_keyno++)
		{
			if (fc_bitmask != HIGHBIT)
				fc_bitmask <<= 1;
			else
			{
				fc_bitP += 1;
				*fc_bitP = 0x0;
				fc_bitmask = 1;
			}

			if (!fc_tuple->bt_columns[fc_keyno].bv_allnulls)
				continue;

			*fc_bitP |= fc_bitmask;
		}
		/* hasnulls位跟随 */
		for (fc_keyno = 0; fc_keyno < fc_brdesc->bd_tupdesc->natts; fc_keyno++)
		{
			if (fc_bitmask != HIGHBIT)
				fc_bitmask <<= 1;
			else
			{
				fc_bitP += 1;
				*fc_bitP = 0x0;
				fc_bitmask = 1;
			}

			if (!fc_tuple->bt_columns[fc_keyno].bv_hasnulls)
				continue;

			*fc_bitP |= fc_bitmask;
		}
	}

	if (fc_tuple->bt_placeholder)
		fc_rettuple->bt_info |= BRIN_PLACEHOLDER_MASK;

	if (fc_tuple->bt_empty_range)
		fc_rettuple->bt_info |= BRIN_EMPTY_RANGE_MASK;

	*fc_size = fc_len;
	return fc_rettuple;
}

/*
 * 生成一个没有数据值的新磁盘元组，标记为占位符。
 *
 * 这是brin_form_tuple的简化版本。
 */
BrinTuple * brin_form_placeholder_tuple(BrinDesc *fc_brdesc, BlockNumber fc_blkno, Size *fc_size)
{
	Size		fc_len;
	Size		fc_hoff;
	BrinTuple  *fc_rettuple;
	int			fc_keyno;
	bits8	   *fc_bitP;
	int			fc_bitmask;

	/* 计算所需的总空间：始终添加空值 */
	fc_len = SizeOfBrinTuple;
	fc_len += BITMAPLEN(fc_brdesc->bd_tupdesc->natts * 2);
	fc_len = fc_hoff = MAXALIGN(fc_len);

	fc_rettuple = palloc0(fc_len);
	fc_rettuple->bt_blkno = fc_blkno;
	fc_rettuple->bt_info = fc_hoff;
	fc_rettuple->bt_info |= BRIN_NULLS_MASK | BRIN_PLACEHOLDER_MASK | BRIN_EMPTY_RANGE_MASK;

	fc_bitP = ((bits8 *) ((char *) fc_rettuple + SizeOfBrinTuple)) - 1;
	fc_bitmask = HIGHBIT;
	/* 对所有属性设置allnulls为true */
	for (fc_keyno = 0; fc_keyno < fc_brdesc->bd_tupdesc->natts; fc_keyno++)
	{
		if (fc_bitmask != HIGHBIT)
			fc_bitmask <<= 1;
		else
		{
			fc_bitP += 1;
			*fc_bitP = 0x0;
			fc_bitmask = 1;
		}

		*fc_bitP |= fc_bitmask;
	}
	/* 不需要设置hasnulls */

	*fc_size = fc_len;
	return fc_rettuple;
}

/*
 * 释放由brin_form_tuple创建的元组
 */
void brin_free_tuple(BrinTuple *fc_tuple)
{
	pfree(fc_tuple);
}

/*
 * 给定大小为len的brin元组，创建它的副本。如果'dest'不是
 * NULL，则其大小为destsz，可以作为输出缓冲区使用；如果要
 * 复制的元组不适合，则通过repalloc扩展，并更新大小以匹配。
 * 这避免了在处理多个brin元组时的palloc/free循环。
 */
BrinTuple * brin_copy_tuple(BrinTuple *fc_tuple, Size fc_len, BrinTuple *fc_dest, Size *fc_destsz)
{
	if (!fc_destsz || *fc_destsz == 0)
		fc_dest = palloc(fc_len);
	else if (fc_len > *fc_destsz)
	{
		fc_dest = repalloc(fc_dest, fc_len);
		*fc_destsz = fc_len;
	}

	memcpy(fc_dest, fc_tuple, fc_len);

	return fc_dest;
}

/*
 * 返回两个BrinTuples是否位级相同。
 */
bool brin_tuples_equal(const BrinTuple *fc_a, Size fc_alen, const BrinTuple *fc_b, Size fc_blen)
{
	if (fc_alen != fc_blen)
		return false;
	if (memcmp(fc_a, fc_b, fc_alen) != 0)
		return false;
	return true;
}

/*
 * 从头开始创建一个新的BrinMemTuple，并初始化为一个空
 * 状态。
 *
 * 注意：我们不提供释放变形元组的任何方法，因此请确保
 * 使用临时内存上下文。
 */
BrinMemTuple * brin_new_memtuple(BrinDesc *fc_brdesc)
{
	BrinMemTuple *fc_dtup;
	long		fc_basesize;

	fc_basesize = MAXALIGN(sizeof(BrinMemTuple) +
						sizeof(BrinValues) * fc_brdesc->bd_tupdesc->natts);
	fc_dtup = palloc0(fc_basesize + sizeof(Datum) * fc_brdesc->bd_totalstored);

	fc_dtup->bt_values = palloc(sizeof(Datum) * fc_brdesc->bd_totalstored);
	fc_dtup->bt_allnulls = palloc(sizeof(bool) * fc_brdesc->bd_tupdesc->natts);
	fc_dtup->bt_hasnulls = palloc(sizeof(bool) * fc_brdesc->bd_tupdesc->natts);

	fc_dtup->bt_empty_range = true;

	fc_dtup->bt_context = AllocSetContextCreate(CurrentMemoryContext,
											 "brin dtuple",
											 ALLOCSET_DEFAULT_SIZES);

	brin_memtuple_initialize(fc_dtup, fc_brdesc);

	return fc_dtup;
}

/*
 * 将BrinMemTuple重置为初始状态。我们返回相同的元组，以
 * 便于记述。
 */
BrinMemTuple * brin_memtuple_initialize(BrinMemTuple *fc_dtuple, BrinDesc *fc_brdesc)
{
	int			fc_i;
	char	   *fc_currdatum;

	MemoryContextReset(fc_dtuple->bt_context);

	fc_currdatum = (char *) fc_dtuple +
		MAXALIGN(sizeof(BrinMemTuple) +
				 sizeof(BrinValues) * fc_brdesc->bd_tupdesc->natts);
	for (fc_i = 0; fc_i < fc_brdesc->bd_tupdesc->natts; fc_i++)
	{
		fc_dtuple->bt_columns[fc_i].bv_attno = fc_i + 1;
		fc_dtuple->bt_columns[fc_i].bv_allnulls = true;
		fc_dtuple->bt_columns[fc_i].bv_hasnulls = false;
		fc_dtuple->bt_columns[fc_i].bv_values = (Datum *) fc_currdatum;

		fc_dtuple->bt_columns[fc_i].bv_mem_value = PointerGetDatum(NULL);
		fc_dtuple->bt_columns[fc_i].bv_serialize = NULL;
		fc_dtuple->bt_columns[fc_i].bv_context = fc_dtuple->bt_context;

		fc_currdatum += sizeof(Datum) * fc_brdesc->bd_info[fc_i]->oi_nstored;
	}

	fc_dtuple->bt_empty_range = true;

	return fc_dtuple;
}

/*
 * 将BrinTuple转换回BrinMemTuple。这是
 * brin_form_tuple的逆操作。
 *
 * 作为一种优化，调用者可以传递先前分配的'dMemtuple'。
 * 这避免在这里分配它，这在多次循环调用此
 * 函数时非常有用。确保给定的BrinMemTuple与我们
 * 需要的匹配是调用者的责任。
 *
 * 注意，我们在这里不需要“磁盘上的tupdesc”；我们依赖于自己的例程
 * 从磁盘格式中解构元组。
 */
BrinMemTuple * brin_deform_tuple(BrinDesc *fc_brdesc, BrinTuple *fc_tuple, BrinMemTuple *fc_dMemtuple)
{
	BrinMemTuple *fc_dtup;
	Datum	   *fc_values;
	bool	   *fc_allnulls;
	bool	   *fc_hasnulls;
	char	   *fc_tp;
	bits8	   *fc_nullbits;
	int			fc_keyno;
	int			fc_valueno;
	MemoryContext fc_oldcxt;

	fc_dtup = fc_dMemtuple ? brin_memtuple_initialize(fc_dMemtuple, fc_brdesc) :
		brin_new_memtuple(fc_brdesc);

	if (BrinTupleIsPlaceholder(fc_tuple))
		fc_dtup->bt_placeholder = true;

	/* 范围开始为空，取决于BrinTuple */
	if (!BrinTupleIsEmptyRange(fc_tuple))
		fc_dtup->bt_empty_range = false;

	fc_dtup->bt_blkno = fc_tuple->bt_blkno;

	fc_values = fc_dtup->bt_values;
	fc_allnulls = fc_dtup->bt_allnulls;
	fc_hasnulls = fc_dtup->bt_hasnulls;

	fc_tp = (char *) fc_tuple + BrinTupleDataOffset(fc_tuple);

	if (BrinTupleHasNulls(fc_tuple))
		fc_nullbits = (bits8 *) ((char *) fc_tuple + SizeOfBrinTuple);
	else
		fc_nullbits = NULL;
	fc_brin_deconstruct_tuple(fc_brdesc,
						   fc_tp, fc_nullbits, BrinTupleHasNulls(fc_tuple),
						   fc_values, fc_allnulls, fc_hasnulls);

	/*
	 * 迭代以将每个值分配给每一列的值数组中的相应项。
	 * 复制发生在元组的上下文中。
	 */
	fc_oldcxt = MemoryContextSwitchTo(fc_dtup->bt_context);
	for (fc_valueno = 0, fc_keyno = 0; fc_keyno < fc_brdesc->bd_tupdesc->natts; fc_keyno++)
	{
		int			fc_i;

		if (fc_allnulls[fc_keyno])
		{
			fc_valueno += fc_brdesc->bd_info[fc_keyno]->oi_nstored;
			continue;
		}

		/*
		 * 在某些情况下，我们希望跳过datumCopy'ing值的数据，
		 * 由调用者决定 ...
		 */
		for (fc_i = 0; fc_i < fc_brdesc->bd_info[fc_keyno]->oi_nstored; fc_i++)
			fc_dtup->bt_columns[fc_keyno].bv_values[fc_i] =
				datumCopy(fc_values[fc_valueno++],
						  fc_brdesc->bd_info[fc_keyno]->oi_typcache[fc_i]->typbyval,
						  fc_brdesc->bd_info[fc_keyno]->oi_typcache[fc_i]->typlen);

		fc_dtup->bt_columns[fc_keyno].bv_hasnulls = fc_hasnulls[fc_keyno];
		fc_dtup->bt_columns[fc_keyno].bv_allnulls = false;

		fc_dtup->bt_columns[fc_keyno].bv_mem_value = PointerGetDatum(NULL);
		fc_dtup->bt_columns[fc_keyno].bv_serialize = NULL;
		fc_dtup->bt_columns[fc_keyno].bv_context = fc_dtup->bt_context;
	}

	MemoryContextSwitchTo(fc_oldcxt);

	return fc_dtup;
}

/*
 * brin_deconstruct_tuple
 *		从磁盘上的BRIN元组提取属性的核心。
 *
 * 它的参数是：
 *	brdesc		存储元组的BRIN描述符
 *	tp			pointer to the tuple data area
 *	nullbits	pointer to the tuple nulls bitmask
 *	nulls		"has nulls"位在元组信息掩码中
 *	values		输出值，数组大小为brdesc->bd_totalstored
 *	allnulls	输出"allnulls"，大小为brdesc->bd_tupdesc->natts
 *	hasnulls	输出"hasnulls"，大小为brdesc->bd_tupdesc->natts
 *
 * 输出数组必须由调用者分配。
 */
static inline void fc_brin_deconstruct_tuple(BrinDesc *fc_brdesc,
					   char *fc_tp, bits8 *fc_nullbits, bool fc_nulls,
					   Datum *fc_values, bool *fc_allnulls, bool *fc_hasnulls)
{
	int			fc_attnum;
	int			fc_stored;
	TupleDesc	fc_diskdsc;
	long		fc_off;

	/*
	 * 首先迭代获取每个属性的空标志。
	 * 请注意，我们反转了 att_isnull 测试的含义，因为我们为空值存储
	 * 1（而不是为非空值如其他地方使用的 att_isnull 约定中的 1）。
	 * 参见 brin_form_tuple。
	 */
	for (fc_attnum = 0; fc_attnum < fc_brdesc->bd_tupdesc->natts; fc_attnum++)
	{
		/*
		 * “所有为空”位表示该列在页范围内的所有值都是空值。
		 * 因此，元组数据区域中没有值。
		 */
		fc_allnulls[fc_attnum] = fc_nulls && !att_isnull(fc_attnum, fc_nullbits);

		/*
		 * “有空值”位表示某些元组有空值，但其他元组有非空值。
		 * 因此，我们知道该元组包含此列的数据。
		 *
		 * hasnulls 位在同一位掩码中跟随 allnulls 位。
		 */
		fc_hasnulls[fc_attnum] =
			fc_nulls && !att_isnull(fc_brdesc->bd_tupdesc->natts + fc_attnum, fc_nullbits);
	}

	/*
	 * 迭代以获取每个属性的存储值。请注意，由于我们
	 * 可能会为多个列重用属性条目，因此我们不能在这里缓存偏移量。
	 */
	fc_diskdsc = fc_brtuple_disk_tupdesc(fc_brdesc);
	fc_stored = 0;
	fc_off = 0;
	for (fc_attnum = 0; fc_attnum < fc_brdesc->bd_tupdesc->natts; fc_attnum++)
	{
		int			fc_datumno;

		if (fc_allnulls[fc_attnum])
		{
			fc_stored += fc_brdesc->bd_info[fc_attnum]->oi_nstored;
			continue;
		}

		for (fc_datumno = 0;
			 fc_datumno < fc_brdesc->bd_info[fc_attnum]->oi_nstored;
			 fc_datumno++)
		{
			Form_pg_attribute fc_thisatt = TupleDescAttr(fc_diskdsc, fc_stored);

			if (fc_thisatt->attlen == -1)
			{
				fc_off = att_align_pointer(fc_off, fc_thisatt->attalign, -1,
										fc_tp + fc_off);
			}
			else
			{
				/* 不是 varlena，因此可以安全使用 att_align_nominal */
				fc_off = att_align_nominal(fc_off, fc_thisatt->attalign);
			}

			fc_values[fc_stored++] = fetchatt(fc_thisatt, fc_tp + fc_off);

			fc_off = att_addlength_pointer(fc_off, fc_thisatt->attlen, fc_tp + fc_off);
		}
	}
}
